04. Test Repository with Constructor Dependency Injection and DI
L5 P2 A10 Test The Repository Using Constructor Dependency Injection And DI V2
In this step you'll implement Constructor Dependency Injection. Constructor dependency injection allows you to swap in the test double by passing it into the constructor.
Step 1: Use Constructor Dependency Injection in DefaultTasksRepository
- Change the
DefaultTaskRepository's constructor from taking in an Application to taking in both data sources and the coroutine dispatcher :
DefaultTasksRepository.kt
// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }
// WITH
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
- Because we passed the dependencies in, remove the init method. You no longer need to create the dependencies.
- Also delete the old instance variables. You're defining them in the constructor:
DefaultTasksRepository.kt
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
- Finally, update the
getRepositorymethod to use the new constructor:
DefaultTasksRepository.kt
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}
You are now using constructor dependency injection!
Step 2: Use your FakeDataSource in your tests
Now that your code is using constructor dependency injection, you can use your fake data source to test your DefaultTasksRepository.
- Right click on the
DefaultTasksRepositoryclass name and select Generate then Test. - Follow the prompts to create
DefaultTasksRepositoryTestin the test source set. - At the top of your new
DefaultTasksRepositoryTestclass, add these member variables to represent the data in your fake data sources:
DefaultTasksRepositoryTest.kt
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }
- Also create three variables - two
FakeDataSourcemember variables (one for each data source for your repository) and a variable for theDefaultTasksRepositorywhich you will test:
DefaultTasksRepositoryTest.kt
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
Now you'll make a method to setup and initialize a testable DefaultTasksRepository. This DefaultTasksRepository will use your test double FakeDataSources.
- Create a method called
createRepositoryand annotate it with@Before. - Instantiate your fake data sources, using the
remoteTasksandlocalTaskslists. - Instantiate your
tasksRepository, using the two fake data sources you just created andDispatchers.Unconfined.
The final method should look like this:
DefaultTasksRepositoryTest.kt
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}
Step 3: Write DefaultTasksRepository's getTasks() Test
Time to write a DefaultTasksRepository test!
- Write a test for the repository's
getTasksmethod. Check that when you call get tasks with true (meaning that it should reload from the remote data source) that it returns data from the remote data source (as opposed to the local data source):
DefaultTasksRepositoryTest.kt
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource(){
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}
You will get an error when you call getTasks:
Step 4: Add runBlockingTest
- Add the required dependencies for testing coroutines to the test source set by using
testImplementation:
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
Don't forget to sync!
You should use runBlockingTest in your test classes when you're calling a suspend function.
- Add the @ExperimentalCoroutinesApi above the class.
- Back in your
DefaultTasksRepositoryTestadd runBlockingTest so that it takes in your entire test as a "block" of code
This final test looks like:
DefaultTasksRepositoryTest.kt
@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}
}
- Run your new
getTasks_requestsAllTasksFromRemoteDataSourcetest and confirm it works and the error is gone! Splendid!